package dataserver;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Arrays;

import masterserver.MasterServer;

import shared.*;

public class DataServer extends java.rmi.server.UnicastRemoteObject implements
	DataServerInterface
{
    private MasterServerInterface remoteMasterServer;

    private DataServerState myState = new DataServerState();

    public String hello()
    {
	return "DataServer Connected";
    }

    public DataServer(String hostname) throws RemoteException
    {
	super(Constants.PORT);

	// clear the directory at start up
	File folder = new File(Constants.DATASERVER_DATASET_DIR);
	File[] files = folder.listFiles();
	for (File file : files)
	    file.delete();

	this.connectToMasterServer();

	this.myState.setHostName(hostname);

	updateMyState();

	Registry registry = LocateRegistry
		.createRegistry(Constants.REGISTRY_PORT);
	registry.rebind(Constants.DATASERVER_NAME, this);

	try
	{
	    this.myState.setRemoteDataServer((DataServerInterface) registry
		    .lookup(Constants.DATASERVER_NAME));
	} catch (NotBoundException e)
	{
	    System.out
		    .println("Problem getting remote reference to data server (itself)! "
			    + e.getMessage());
	}

	int i = 0;
	while (i == 0)
	{
	    try
	    {
		remoteMasterServer.registerDataserver(myState);
		System.out.println("Successfully registered on masterserver!");
		break;
	    } catch (Exception e)
	    {
		i++;
		this.connectToMasterServer();
		continue;

	    }
	}

	new Thread(new RegisterOnMaster()).start();

    }

    // get remote reference to Master Server
    public void connectToMasterServer()
    {
	Registry registry = null;
	boolean allFail = true;
	try
	{
	    if (allFail == true
		    && InetAddress.getByName(Constants.MASTER_SERVER_1_IP)
			    .isReachable(Constants.IP_WAIT_TIME))
	    {
		try
		{

		    registry = LocateRegistry
			    .getRegistry(Constants.MASTER_SERVER_1_IP);
		    this.remoteMasterServer = (MasterServerInterface) registry
			    .lookup(Constants.MASTER_SERVER_NAME);
		    allFail = false;

		} catch (Exception e1)
		{
		    System.out
			    .println("Failed to connect to master server 1, now trying master server 2!");
		}
	    }
	} catch (Exception eee)
	{
	}

	try
	{
	    if (allFail == true
		    && InetAddress.getByName(Constants.MASTER_SERVER_2_IP)
			    .isReachable(Constants.IP_WAIT_TIME))
	    {
		try
		{
		    registry = LocateRegistry
			    .getRegistry(Constants.MASTER_SERVER_2_IP);
		    this.remoteMasterServer = (MasterServerInterface) registry
			    .lookup(Constants.MASTER_SERVER_NAME);
		    allFail = false;

		} catch (Exception e2)
		{
		    System.out
			    .println("Failed to connect to master server 2, now trying master server 3!");
		}
	    }
	} catch (Exception eee)
	{
	}

	try
	{
	    if (allFail == true
		    && InetAddress.getByName(Constants.MASTER_SERVER_3_IP)
			    .isReachable(Constants.IP_WAIT_TIME))
	    {
		try
		{

		    registry = LocateRegistry
			    .getRegistry(Constants.MASTER_SERVER_3_IP);
		    this.remoteMasterServer = (MasterServerInterface) registry
			    .lookup(Constants.MASTER_SERVER_NAME);

		} catch (Exception e3)
		{
		    System.out
			    .println("All master servers are down! Now wait..");
		}
	    }
	} catch (Exception eee)
	{
	}
    }

    public void updateMyState()
    {
	try
	{
	    if (this.myState.getHostName() == null
		    || this.myState.getHostName().length() == 0)
		this.myState.setHostName(InetAddress.getLocalHost()
			.getCanonicalHostName());
	} catch (UnknownHostException e1)
	{
	    e1.printStackTrace();
	}

	File folder = new File(Constants.DATASERVER_DATASET_DIR);
	File[] files = folder.listFiles();

	this.myState.setNumberOfFiles(files.length);
	this.myState.setAvailableSpace(new File("/").getUsableSpace());

	ArrayList<Long> fileIDs = new ArrayList<Long>();
	for (int i = 0; i < files.length; i++)
	{
	    try
	    {
		Long fileID = Long.parseLong(files[i].getName());
		fileIDs.add(fileID);
	    } catch (Exception e)
	    {
		System.out.println("Not an integer name: "
			+ files[i].toString());
	    }
	}
	this.myState.setFileIDs(fileIDs);
    }

    @Override
    public DataChunk readData(long fileID, long offset) throws RemoteException
    {
	Path pathname = Paths.get(Constants.DATASERVER_DATASET_DIR + fileID);
	if (!Files.exists(pathname))
	    throw new CloudException("File does not exist!");
	SeekableByteChannel channel = null;
	byte[] bytes = new byte[Constants.DATA_CHUNK_SIZE];

	try
	{
	    System.out.println("Reading file " + fileID + "offset " + offset);
	    channel = Files.newByteChannel(pathname);
	    channel.position(offset);

	    ByteBuffer readBuffer = ByteBuffer.wrap(bytes);

	    int length = channel.read(readBuffer);
	    if (length < 0)
		length = 0;

	    byte[] newBytes = Arrays.copyOf(bytes, length);

	    DataChunk dataChunk = new DataChunk(newBytes);
	    dataChunk.setOffset(offset);

	    ByteBuffer remainderBuffer = ByteBuffer.allocate(1);
	    int remainder = channel.read(remainderBuffer);

	    if (length == 0 || remainder <= 0)
		dataChunk.setLast(true);
	    else if (length > 0 && remainder > 0)
		dataChunk.setLast(false);

	    System.out.println("Completed one chunk reading request of file: "
		    + fileID + " Is last chunk? " + dataChunk.isLast());
	    return dataChunk;
	} catch (Exception e)
	{
	    e.printStackTrace();
	    throw new CloudException("Failed to read file!");
	} finally
	{
	    try
	    {
		channel.close();
	    } catch (IOException e)
	    {
		e.printStackTrace();
	    }
	}

    }

    @Override
    public void writeData(FileDescriptor fileDescriptor, DataChunk dataChunk)
	    throws RemoteException
    {
	Path pathname = Paths.get(Constants.DATASERVER_DATASET_DIR
		+ fileDescriptor.getFileID());

	if (dataChunk.getOffset() == 0)
	{
	    if (Files.exists(pathname))
	    {
		try
		{
		    Files.delete(pathname);
		    Files.createFile(pathname);
		} catch (Exception e)
		{
		    e.printStackTrace();
		}
	    } else
	    {
		try
		{
		    Files.createFile(pathname);
		} catch (IOException e)
		{
		    e.printStackTrace();
		}
	    }
	}

	SeekableByteChannel channel = null;
	try
	{
	    channel = Files.newByteChannel(pathname,
		    java.nio.file.StandardOpenOption.WRITE);
	    channel.position(dataChunk.getOffset());
	    ByteBuffer writeBuffer = ByteBuffer.wrap(dataChunk.getContent());
	    int bytesWritten = channel.write(writeBuffer);
	    System.out.println("Finished writing one chunk..");

	    if (bytesWritten != dataChunk.getContent().length)
		throw new CloudException("Incomplete write! Failure!");
	    channel.close();

	    try
	    {
		if (dataChunk.isLast())
		{
		    String newHash = Utilities.computeHash(pathname.toString());
		    fileDescriptor.setHash(newHash);
		    this.remoteMasterServer.completeWrite(myState,
			    fileDescriptor);
		    System.out
			    .println("Completed one writing request of file: "
				    + fileDescriptor.getFileID());
		}
	    } catch (Exception e)
	    {
		e.printStackTrace();
		this.connectToMasterServer();
		this.remoteMasterServer.completeWrite(myState, fileDescriptor);
	    }

	} catch (Exception e)
	{
	    System.out.println("Failed to write data! " + e.getMessage());
	    throw new CloudException("Failed to write data!");
	} finally
	{
	    try
	    {
		if (channel.isOpen())
		    channel.close();
	    } catch (IOException e)
	    {
		e.printStackTrace();
	    }
	}
    }

    @Override
    public void createFile(long newFileID) throws RemoteException
    {

	Path pathname = Paths.get(Constants.DATASERVER_DATASET_DIR + newFileID);

	try
	{
	    Files.createFile(pathname);
	    System.out.println("Successfully created the file: " + newFileID);
	} catch (FileAlreadyExistsException e1)
	{
	    System.out
		    .println("File already exists, no need to re-create the file: "
			    + newFileID);

	} catch (Exception e2)
	{
	    e2.printStackTrace();
	    throw new CloudException("Failed to created new file!");
	}

    }

    class RegisterOnMaster implements Runnable
    {

	@Override
	public void run()
	{
	    while (true)
	    {
		try
		{
		    Thread.sleep(Constants.DATASERVER_EXPIRY_LENGTH / 3);
		} catch (InterruptedException e)
		{
		    e.printStackTrace();
		}

		updateMyState();
		int i = 0;
		while (i == 0)
		{
		    try
		    {
			remoteMasterServer.registerDataserver(myState);
			System.out
				.println("Sent heartbeat to masterserver successfully!");
			break;
		    } catch (Exception e)
		    {
			i++;
			connectToMasterServer();
			continue;

		    }
		}
		if (i == 1)
		{
		    System.out
			    .println("Failed to register on master server! Now wait..");
		}
	    }

	}

    }

    public static void main(String[] args)
    {
	if (args.length == 0)
	{
	    System.out.println("usage: DataServer DataserverHostaddress");
	    System.exit(-1);
	}

	String hostname = args[0];

	System.setProperty("java.rmi.server.hostname", hostname);

	DataServer dataServer = null;

	try
	{
	    // hostname must be passed to data server; data server itself cannot
	    // resolve its FQDN correctly
	    dataServer = new DataServer(hostname);
	} catch (Exception e)
	{
	    e.printStackTrace();
	    System.exit(1);
	}

    }

}
